1 /** 2 The different TestCase classes 3 */ 4 module unit_threaded.testcase; 5 6 private shared(bool) _stacktrace = false; 7 8 private void setStackTrace(bool value) @trusted nothrow @nogc { 9 synchronized { 10 _stacktrace = value; 11 } 12 } 13 14 /// Let AssertError(s) propagate and thus dump a stacktrace. 15 public void enableStackTrace() @safe nothrow @nogc { 16 setStackTrace(true); 17 } 18 19 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran. 20 public void disableStackTrace() @safe nothrow @nogc { 21 setStackTrace(false); 22 } 23 24 /** 25 * Class from which other test cases derive 26 */ 27 class TestCase { 28 29 import unit_threaded.io : Output; 30 31 /** 32 * Returns: the name of the test 33 */ 34 string getPath() const pure nothrow { 35 return this.classinfo.name; 36 } 37 38 /** 39 * Executes the test. 40 * Returns: array of failures (child classes may have more than 1) 41 */ 42 string[] opCall() { 43 static if (__VERSION__ >= 2077) 44 import std.datetime.stopwatch : StopWatch, AutoStart; 45 else 46 import std.datetime : StopWatch, AutoStart; 47 48 currentTest = this; 49 auto sw = StopWatch(AutoStart.yes); 50 doTest(); 51 flushOutput(); 52 return _failed ? [getPath()] : []; 53 } 54 55 /** 56 Certain child classes override this 57 */ 58 ulong numTestsRun() const { 59 return 1; 60 } 61 62 void showChrono() @safe pure nothrow { 63 _showChrono = true; 64 } 65 66 void setOutput(Output output) @safe pure nothrow { 67 _output = output; 68 } 69 70 package: 71 72 static TestCase currentTest; 73 Output _output; 74 75 void silence() @safe pure nothrow { 76 _silent = true; 77 } 78 79 final Output getWriter() @safe { 80 import unit_threaded.io : WriterThread; 81 82 return _output is null ? WriterThread.get : _output; 83 } 84 85 protected: 86 87 abstract void test(); 88 void setup() { 89 } ///override to run before test() 90 void shutdown() { 91 } ///override to run after test() 92 93 private: 94 95 bool _failed; 96 bool _silent; 97 bool _showChrono; 98 99 final auto doTest() { 100 import std.conv : text; 101 import std.datetime : Duration; 102 103 static if (__VERSION__ >= 2077) 104 import std.datetime.stopwatch : StopWatch, AutoStart; 105 else 106 import std.datetime : StopWatch, AutoStart; 107 108 auto sw = StopWatch(AutoStart.yes); 109 print(getPath() ~ ":\n"); 110 check(setup()); 111 check(test()); 112 check(shutdown()); 113 if (_failed) 114 print("\n"); 115 if (_showChrono) 116 print(text(" (", cast(Duration) sw.peek, ")\n\n")); 117 if (_failed) 118 print("\n"); 119 } 120 121 final bool check(E)(lazy E expression) { 122 import unit_threaded.should : UnitTestException; 123 124 try { 125 expression(); 126 } catch (UnitTestException ex) { 127 fail(ex.toString()); 128 } catch (Throwable ex) { 129 fail("\n " ~ ex.toString() ~ "\n"); 130 } 131 132 return !_failed; 133 } 134 135 final void fail(in string msg) { 136 _failed = true; 137 print(msg); 138 } 139 140 final void print(in string msg) { 141 import unit_threaded.io : write; 142 143 if (!_silent) 144 getWriter.write(msg); 145 } 146 147 final void flushOutput() { 148 getWriter.flush; 149 } 150 } 151 152 /** 153 A test that runs other tests. 154 */ 155 class CompositeTestCase : TestCase { 156 void add(TestCase t) { 157 _tests ~= t; 158 } 159 160 void opOpAssign(string op : "~")(TestCase t) { 161 add(t); 162 } 163 164 override string[] opCall() { 165 import std.algorithm : map, reduce; 166 167 return _tests.map!(a => a()).reduce!((a, b) => a ~ b); 168 } 169 170 override void test() { 171 assert(false, "CompositeTestCase.test should never be called"); 172 } 173 174 override ulong numTestsRun() const { 175 return _tests.length; 176 } 177 178 package TestCase[] tests() @safe pure nothrow { 179 return _tests; 180 } 181 182 override void showChrono() { 183 foreach (test; _tests) 184 test.showChrono; 185 } 186 187 private: 188 189 TestCase[] _tests; 190 } 191 192 /** 193 A test that should fail 194 */ 195 class ShouldFailTestCase : TestCase { 196 this(TestCase testCase, in TypeInfo exceptionTypeInfo) { 197 this.testCase = testCase; 198 this.exceptionTypeInfo = exceptionTypeInfo; 199 } 200 201 override string getPath() const pure nothrow { 202 return this.testCase.getPath; 203 } 204 205 override void test() { 206 import unit_threaded.should : UnitTestException; 207 import std.exception : enforce, collectException; 208 import std.conv : text; 209 210 const ex = collectException!Throwable(testCase.test()); 211 enforce!UnitTestException(ex !is null, 212 "Test '" ~ testCase.getPath ~ "' was expected to fail but did not"); 213 enforce!UnitTestException(exceptionTypeInfo is null 214 || typeid(ex) == exceptionTypeInfo, text("Test '", testCase.getPath, 215 "' was expected to throw ", exceptionTypeInfo, " but threw ", typeid(ex))); 216 } 217 218 private: 219 220 TestCase testCase; 221 const(TypeInfo) exceptionTypeInfo; 222 } 223 224 /** 225 A test that is a regular function. 226 */ 227 class FunctionTestCase : TestCase { 228 229 import unit_threaded.reflection : TestData, TestFunction; 230 231 this(in TestData data) pure nothrow { 232 _name = data.getPath; 233 _func = data.testFunction; 234 } 235 236 override void test() { 237 _func(); 238 } 239 240 override string getPath() const pure nothrow { 241 return _name; 242 } 243 244 private string _name; 245 private TestFunction _func; 246 } 247 248 /** 249 A test that is a `unittest` block. 250 */ 251 class BuiltinTestCase : FunctionTestCase { 252 253 import unit_threaded.reflection : TestData; 254 255 this(in TestData data) pure nothrow { 256 super(data); 257 } 258 259 override void test() { 260 import core.exception : AssertError; 261 262 try 263 super.test(); 264 catch (AssertError e) { 265 import unit_threaded.should : fail; 266 267 fail(_stacktrace ? e.toString() : e.msg, e.file, e.line); 268 } 269 } 270 } 271 272 /** 273 A test that is expected to fail some of the time. 274 */ 275 class FlakyTestCase : TestCase { 276 this(TestCase testCase, int retries) { 277 this.testCase = testCase; 278 this.retries = retries; 279 } 280 281 override string getPath() const pure nothrow { 282 return this.testCase.getPath; 283 } 284 285 override void test() { 286 287 foreach (i; 0 .. retries) { 288 try { 289 testCase.test; 290 break; 291 } catch (Throwable t) { 292 if (i == retries - 1) 293 throw t; 294 } 295 } 296 } 297 298 private: 299 300 TestCase testCase; 301 int retries; 302 }